using SharpGraphT.Sharp.Sharp.Util;
using SharpGraphT.Util;

namespace SharpGraphT.Graph.Specifics;

[Serializable]
public class DirectedSpecifics<TV, TE> : ISpecifics<TV, TE>
    where TE : class, new() {
    private static readonly long SerialVersionUid = 5964807709682219859L;

    protected IGraph<TV, TE> graph;
    protected IDictionary<TV, DirectedEdgeContainer<TV, TE>> vertexMap;
    protected IEdgeSetFactory<TV, TE> edgeSetFactory;

    public DirectedSpecifics(IGraph<TV, TE> graph,
        IDictionary<TV, DirectedEdgeContainer<TV, TE>> vertexMap,
        IEdgeSetFactory<TV, TE> edgeSetFactory) {
        this.graph = Objects.RequireNonNull(graph);
        this.vertexMap = Objects.RequireNonNull(vertexMap);
        this.edgeSetFactory = Objects.RequireNonNull(edgeSetFactory);
    }

    public bool AddVertex(TV v) {
        Objects.RequireNonDefault(v,
            "Vertex of a default value should not be added");
        
        if (vertexMap.TryGetValue(v, out var ec)) {
            return false;
        }

        vertexMap[v] = new DirectedEdgeContainer<TV, TE>(edgeSetFactory, v);
        return true;
    }

    public bool RemoveVertex(TV v) {
        if (!vertexMap.ContainsKey(v)) {
            return false;
        }

        vertexMap.Remove(v);
        return true;
    }

    public IReadOnlySet<TV> VertexSet => vertexMap.Keys.ToHashSet();

    public virtual IReadOnlySet<TE> GetAllEdges(TV sourceVertex,
        TV targetVertex) {
        if (!graph.ContainsVertex(sourceVertex) ||
            !graph.ContainsVertex(targetVertex)) {
            return null;
        }

        var edges = new ArrayUnenforcedSet<TE>();
        edges.AddRange(GetEdgeContainer(sourceVertex).Outgoing.Where(e =>
            graph.GetEdgeTarget(e).Equals(targetVertex)).ToList());
        return edges;
    }

    public virtual TE GetEdge(TV sourceVertex, TV targetVertex) =>
        graph.ContainsVertex(sourceVertex) && graph.ContainsVertex(targetVertex)
            ? GetEdgeContainer(sourceVertex).Outgoing.First(e =>
                graph.GetEdgeTarget(e).Equals(targetVertex))
            : null;

    public virtual bool AddEdgeToTouchingVertices(TV sourceVertex,
        TV targetVertex, TE e) {
        GetEdgeContainer(sourceVertex).AddOutgoingEdge(e);
        GetEdgeContainer(targetVertex).AddIncomingEdge(e);
        return true;
    }

    public virtual bool AddEdgeToTouchingVerticesIfAbsent(TV sourceVertex,
        TV targetVertex, TE e) {
        var ec = GetEdgeContainer(sourceVertex);
        if (ec.Outgoing.Any(e => graph.GetEdgeTarget(e).Equals(targetVertex))) {
            return false;
        }

        ec.AddOutgoingEdge(e);
        GetEdgeContainer(targetVertex).AddIncomingEdge(e);

        return true;
    }

    public virtual TE CreateEdgeToTouchingVerticesIfAbsent(TV sourceVertex,
        TV targetVertex, Func<TE> edgeSupplier) {
        var ec = GetEdgeContainer(sourceVertex);
        if (ec.Outgoing.Any(e => graph.GetEdgeTarget(e).Equals(targetVertex))) {
            return null;
        }

        var e = edgeSupplier();
        ec.AddOutgoingEdge(e);
        GetEdgeContainer(targetVertex).AddIncomingEdge(e);
        return e;
    }

    public int DegreeOf(TV vertex) => InDegreeOf(vertex) + OutDegreeOf(vertex);

    public IReadOnlySet<TE> EdgesOf(TV vertex) {
        var inAndOut = new ArrayUnenforcedSet<TE>(GetEdgeContainer(vertex)
            .Incoming);

        inAndOut.AddRange(graph.Type.IsAllowingSelfLoops
            ? GetEdgeContainer(vertex).Outgoing.Where(e =>
                !vertex.Equals(graph.GetEdgeTarget(e))).ToList()
            : GetEdgeContainer(vertex).Outgoing);

        return inAndOut;
    }

    public int InDegreeOf(TV vertex) =>
        ((ISet<TV>) GetEdgeContainer(vertex).Incoming).Count;

    public IReadOnlySet<TE> IncomingEdgesOf(TV vertex) =>
        GetEdgeContainer(vertex).GetUnmodifiableIncomingEdges();

    public int OutDegreeOf(TV vertex) =>
        ((ISet<TV>) GetEdgeContainer(vertex).Outgoing).Count;

    public IReadOnlySet<TE> OutgoingEdgesOf(TV vertex) =>
        GetEdgeContainer(vertex).GetUnmodifiableOutgoingEdges();

    public virtual void RemoveEdgeFromTouchingVertices(TV sourceVertex,
        TV targetVertex, TE e) {
        GetEdgeContainer(sourceVertex).RemoveOutgoingEdge(e);
        GetEdgeContainer(targetVertex).RemoveIncomingEdge(e);
    }

    protected DirectedEdgeContainer<TV, TE> GetEdgeContainer(TV vertex) =>
        vertexMap.ContainsKey(vertex)
            ? vertexMap[vertex]
            : vertexMap[vertex] =
                new DirectedEdgeContainer<TV, TE>(edgeSetFactory, vertex);
}